package dev.detekt.rules.coroutines

import dev.detekt.api.Config
import dev.detekt.test.junit.KotlinCoreEnvironmentTest
import dev.detekt.test.lintWithContext
import dev.detekt.test.utils.KotlinEnvironmentContainer
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

@KotlinCoreEnvironmentTest
class RedundantSuspendModifierSpec(val env: KotlinEnvironmentContainer) {

    private val subject = RedundantSuspendModifier(Config.empty)

    @Test
    fun `reports when public function returns expression of platform type`() {
        val code = """
            import kotlin.coroutines.Continuation
            import kotlin.coroutines.resume
            import kotlin.coroutines.suspendCoroutine
            
            suspend fun suspendCoroutine() = suspendCoroutine { continuation: Continuation<String> ->
                continuation.resume("string")
            }
            
            class RedundantSuspend {
                suspend fun redundantSuspend() {
                    println("hello world")
                }
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).hasSize(1)
    }

    @Test
    fun `does not report when private`() {
        val code = """
            import kotlin.coroutines.Continuation
            import kotlin.coroutines.resume
            import kotlin.coroutines.suspendCoroutine
            
            suspend fun suspendCoroutine() = suspendCoroutine { continuation: Continuation<String> ->
                continuation.resume("string")
            }
            
            suspend fun doesSuspend() {
                suspendCoroutine()
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `does not report when public function returns expression of platform type`() {
        val code = """
            class RedundantClass {
                open suspend fun redundantSuspend() {
                    println("hello world")
                }
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `does not report suspend function in interface without body`() {
        val code = """
            interface SuspendInterface {
                suspend fun empty()
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `does not report suspend function in interface with body - #8800`() {
        val code = """
            interface Interface {
                suspend fun foo(): Int = 0
            }
            
            class Impl : Interface {
                override suspend fun foo(): Int {
                    kotlinx.coroutines.delay(1000)
                    return 1
                }
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `does not report abstract suspend function in abstract class`() {
        val code = """
            abstract class BaseClass {
                abstract suspend fun bar(): Int
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `does not report abstract suspend function in sealed class`() {
        val code = """
            sealed class SealedClass {
                abstract suspend fun bar(): Int
                class Subclass : SealedClass() {
                    override suspend fun bar(): Int = 0
                }
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `does not report overridden suspend function`() {
        val code = """
            interface SuspendInterface {
                suspend fun empty()
            }
            
            class SuspendClass : SuspendInterface {
                override suspend fun empty() {
                    println("hello world")
                }
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `does not report actual suspend function`() {
        val code = """
            expect class Foo {
                suspend fun bar()
            }
            
            actual class Foo {
                actual suspend fun bar() {}
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code, allowCompilationErrors = true)).isEmpty()
    }

    @Test
    fun `ignores when iterator is suspending`() {
        val code = """
            class SuspendingIterator {
                suspend operator fun iterator(): Iterator<Any> = iterator { yield("value") }
            }
            
            suspend fun bar() {
                for (x in SuspendingIterator()) {
                    println(x)
                }
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `ignores when suspending function used in property delegate`() {
        val code = """
            class SuspendingIterator {
                suspend operator fun iterator(): Iterator<Any> = iterator { yield("value") }
            }
            
            fun coroutine(block: suspend () -> Unit) {}
            
            suspend fun bar() {
                val lazyValue: String by lazy {
                    coroutine {
                        SuspendingIterator().iterator()
                    }
                    "Hello"
                }
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `does not report when suspend function is called in extension method`() {
        val code = """
            import kotlinx.coroutines.delay
            suspend fun foo() { delay(1000) }
            suspend fun String.bar() {
                foo()
            }
            suspend fun  String.baz() = foo()
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `does not report when coroutineContext is used`() {
        val code = """
            suspend fun a() {
                kotlin.coroutines.coroutineContext
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `does not report if a suspending invoke operator function is called`() {
        val code = """
            class Foo {
                suspend operator fun invoke(ms: Long): Unit = kotlinx.coroutines.delay(ms)
            }

            suspend fun bar() {
                val foo = Foo()
                foo(1234L)
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `does not report if a suspending get operator function is called`() {
        val code = """
            class Foo {
                suspend operator fun get(ms: Long): Unit = kotlinx.coroutines.delay(ms)
            }

            suspend fun bar() {
                val foo = Foo()
                foo[1234L]
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `does not report if a suspending set operator function is called`() {
        val code = """
            class Foo {
                suspend operator fun set(key: String, value: Long): Unit = kotlinx.coroutines.delay(value)
            }

            suspend fun bar() {
                val foo = Foo()
                foo["key"] = 1234L
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }

    @Test
    fun `does not report if a suspending lambda is called`() {
        val code = """
            suspend fun bar(foo: suspend () -> Unit) {
                foo()
            }
        """.trimIndent()
        assertThat(subject.lintWithContext(env, code)).isEmpty()
    }
}
